/*
 * Copyright (c) 2007 Compl Yue Still, All Rights Reserved.
 */

package hbi.wire.as3.guest
{
	import flash.utils.*;
	
	import hbi.as3.Long;
	import hbi.as3.utils.ArrayType;
	import hbi.as3.utils.IFlushableOutput;
	import hbi.wire.ProtocolError;
	
	public class GuestEnd
	{
		
		private var _output:IFlushableOutput;
		
		public function GuestEnd(output:IFlushableOutput)
		{
			this._output = output;
		}
		
		private var _types:Dictionary = new Dictionary();
		private var _typeCount:int = 0;
		
		protected function forecastType(type:Class):ForecastedType
		{
			var forecastedType:ForecastedType = _types[type];
			if(forecastedType) return forecastedType;
			_output.writeByte('t'.charCodeAt());
			_output.writeUTF(getQualifiedClassName(type));
			return _types[type] = new ForecastedType(_output, _typeCount++);
		}
		
		private var _agentType:ForecastedType;
		
		public function startAgent(agentClass:Class):void
		{
			_agentType = forecastType(agentClass);
			_output.writeByte('a'.charCodeAt());
			_output.writeInt(_agentType.index);
		}
		
		public function scriptAgent(methodName:*, args:Array):void
		{
			// use mangling format with number of args to fail fast if wrong
			// number of args are specified
			var mangledMethodName:String = methodName + "#" + args.length;
			var methodIndex:int = _agentType.methodIndex(mangledMethodName);
			_output.writeByte('c'.charCodeAt());
			_output.writeShort(methodIndex);
			for each(var arg:* in args)
				writeObject(arg);
			_output.writeByte('z'.charCodeAt());
		}
		
		public function finishAgent():void
		{
			_agentType = null;
			_output.writeByte('z'.charCodeAt());
			_output.flush();
		}
		
		private function writeObject(value:*):void
		{
			if(value == undefined || value == null)
			{
				_output.writeByte('N'.charCodeAt());
			}
			else if(value is Boolean)
			{
				this.writeBoolean(value);
			}
			else if(value is int)
			{
				_output.writeByte('I'.charCodeAt());
				this.writeInt(value);
			}
			else if(value is Long)
			{
				_output.writeByte('L'.charCodeAt());
				this.writeLong(value);
			}
			else if(value is Number)
			{
				_output.writeByte('D'.charCodeAt());
				this.writeNumber(value);
			}
			else if(value is String)
			{
				_output.writeByte('S'.charCodeAt());
				this.writeString(value);
			}
			else if(value is ByteArray)
			{
				_output.writeByte('B'.charCodeAt());
				this.writeByteArray(value);
			}
			else if(value is Date)
			{
				_output.writeByte('d'.charCodeAt());
				this.writeDate(value);
			}
			else if(value is Array)
			{
				var arrayType:ArrayType = ArrayType.arrayTypeOf(value);
				if(arrayType) // consistently typed
				{
					for(var dim:uint = arrayType.dimension; dim > 0; dim--)
						_output.writeByte('['.charCodeAt());
					switch(arrayType.elementType)
					{
					case Boolean:
						_output.writeByte('T'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeBoolean);
						break;
					case int:
						_output.writeByte('I'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeInt);
						break;
					case Long:
						_output.writeByte('L'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeLong);
						break;
					case Number:
						_output.writeByte('D'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeNumber);
						break;
					case String:
						_output.writeByte('S'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeString);
						break;
					case ByteArray:
						_output.writeByte('B'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeByteArray);
						break;
					case Date:
						_output.writeByte('d'.charCodeAt());
						this.writeTypedArray(value, arrayType.dimension, 
							writeDate);
						break;
					default:
						throw new ProtocolError(
							"Unsupported array element type: "
							+ getQualifiedClassName(arrayType.elementType));
					}
				}
				else // inconsistently typed
				{
					_output.writeByte(']'.charCodeAt());
					this.writeUntypedArray(value);
				}
			}
			else
			{
				throw new Error("Bad object type: " + 
					getQualifiedClassName(value));
			}
		}
		
		private function writeBoolean(value:Boolean):void
		{
			_output.writeByte((value?'T':'F').charCodeAt());
		}
		private function writeInt(value:int):void
		{
			_output.writeInt(value);
		}
		private function writeLong(value:Long):void
		{
			value.writeTo(_output);
		}
		private function writeNumber(value:Number):void
		{
			_output.writeDouble(value);
		}
		private function writeString(value:String):void
		{
			_output.writeUTF(value);
		}
		private function writeByteArray(value:ByteArray):void
		{
			if(value.length > 0xFFFF)
				throw new RangeError("Too large ByteArray: " + value.length);
			_output.writeShort(value.length);
			_output.writeBytes(value);
		}
		private function writeDate(value:Date):void
		{
			new Long(value.getTime()).writeTo(_output);
		}

		private function writeTypedArray
			(value:Array, dimension:int, writer:Function):void
		{
			if(!value)
			{
				// treat missing element as zero-length array
				_output.writeUnsignedInt(0);
				return;
			}
			
			if(value.length > 0x7FFFFFFF)
				// it would turn out to be negative at a Java peer, where
				// int is always a signed number
				throw new RangeError("Array too large: " + value.length);

			_output.writeUnsignedInt(value.length);
			if(value.length < 1) return;
			
			var ele:*;
			if(dimension > 1) // outer array dimension
			{
				dimension--;
				for each (ele in value)
				{
					writeTypedArray(ele, dimension, writer);
				}
			}
			else // inner-most array dimension
			{
				for each (ele in value)
				{
					if(!ele) throw new Error("Array element absence!");
					// assert !(ele is Array);
					writer(ele);
				}
			}
		}
		
		private function writeUntypedArray(value:Array):void
		{
			if(value.length > 0)
			{
				if(value.length > 0x7FFFFFFF)
					// it would turn out to be negative at a Java peer, where
					// int is always a signed number
					throw new RangeError("Array too large: " + value.length);
		
				_output.writeUnsignedInt(value.length);
				for each (var ele:* in value)
					this.writeObject(ele);
			}
			else
			{
				_output.writeUnsignedInt(0);
			}
		}
		
		// not used by far
		private function writeBinaryStream(value:ByteArray):void
		{
			const CHUNK_SIZE:uint = 0x7FFF;
			
			var offset:uint = 0;
			var rest:uint = value.length;
			while(rest > CHUNK_SIZE)
			{
				_output.writeByte('b'.charCodeAt());
				_output.writeShort(CHUNK_SIZE);
				_output.writeBytes(value, offset, CHUNK_SIZE);
				offset += CHUNK_SIZE;
				rest -= CHUNK_SIZE;
			}
			_output.writeByte('B'.charCodeAt());
			_output.writeShort(rest);
			_output.writeBytes(value, offset, rest);
		}
	}
}
